Medium 好讀版點此。
在許多比較早期的 git 教材中,如果提到要切換分支,都是使用 git checkout 指令,但我的文章卻從來沒用過,而是用 git switch,為什麼?
其實 git switch 是 git 的第 2.23 版本才出現的指令,在該版本之前確實都是用 git checkout 切換分支,但 git checkout 的功能還很多,以下我們將檢視幾個較常用的,以及在 2.23 版以後,各自對應、且僅有單一功能的指令。
我們先建立在一個資料夾中,建立第一個 commit:
echo "First version" > file.txt
git add file.txt
git commit -m "Initial commit"
再建立第二個 commit:
echo "Second version" > file.txt
git add file.txt
git commit -m "Second commit"
現在我們有兩個 commit 如下:
回顧 Day 9 的文章,我們知道 HEAD 這項參考用來指出目前在哪個分支,而切換分支其實就是更改 HEAD 指向的參考。
例如現在我們製作 feature 分支:
git branch feature
剛建立時,main 跟 feature 兩項參考都指向同一個 commit,而 HEAD 指向 main:
透過 git switch 轉到 feature 分支:
git switch feature
切換後,HEAD 改指向 feature 這項參考:
改用 git checkout,也可以切換分支,例如現在切換回 main 分支:
git checkout main
則 HEAD 改指回 main 參考:
兩次切換分支的終端機畫面如下:
現在我們針對 file.txt 進行第三次更動,但本次更動只加到預存區,不形成 commit:
echo "Third version" > file.txt
git add file.txt
最後一次更動則只在工作目錄:
echo "Fourth version" > file.txt
現在我們沒有要切換分支,所以也把 feature 這項參考拿掉:
git branch -d feature
此時 git 物件結構長這樣:
目前工作目錄的版本內容為 "Fourth version"、預存區版本內容為 "Third version",如下:
如果想要把工作目錄的檔案改成預存區版本,則可使用以下指令:
git restore file.txt
觀察整體結構,可發現變化如下:
要達到同樣功能,也可以用 git checkout 指令:
git checkout -- file.txt
結果相同如下:
現在我們希望只保留最新 commit 版本,捨棄預存區跟工作目錄版本,可以使用以下指令:
git restore --source=HEAD --staged --worktree file.txt
發生的變化如下:
同樣的目標也可以用 git checkout 達成:
git checkout HEAD -- file.txt
示意圖如下:
"Third version"、工作目錄內容為 "Fourth version" 時,我們依序把兩者都變成一個 commit。首先把本來就在預存區的版本變成commit:
git commit -m "Third commit"
再來把工作目錄的版本也變成 commit:
git add file.txt
git commit -m "Fourth commit"
現在共有四個 commit 如下:
目前 commit、預存區與工作目錄內容如下:
如果我們想退回第一個 commit 4e661dd,可以用以下指令:
git restore --source=4e661dd file.txt
發生的變化如下:
可發現預存區跟 commit 內容,還有 HEAD 與 main 兩參考指向的標的都不變,只有工作目錄內容變成跟指定 commit 相同。
如果要用 git checkout 達成同樣目標呢?
git checkout 4e661dd -- file.txt
但這道指令不只影響工作目錄,也影響預存區,如下:
git checkout <commit> 切到指定 commit,會發生什麼事呢?現在我們一樣在第四個 commit ed1ea06 想切回第一個 commit 4e661dd,因而輸入以下指令:
git checkout 4e661dd
這時候終端機出現看起來很可怕的訊息:
這是怎麼回事呢?
只要我們打開 .git/ 資料夾,便可以觀察到:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # ed1ea06e0f3db055d12ef2c0ea26348ebdab08d1
│ └── tags/
│
├── ...
└── HEAD # 4e661dd7f311990102cb56ed907afcffa137f6f8
畫成圖示如下:
HEAD 自己跑到新 commit,沒有把 main 這個參考帶著走啊!這其實不是什麼可怕的事情,我們可以透過 git switch 或 git checkout 指令,讓 HEAD 再次指向分支。
比如說,如果我們下:
git checkout main
或者新版的:
git switch main
就會讓 HEAD 回去指向 main:
圖示如下:
或者可以用以下指令,原地生出一個新分支,假設命名為 feature:
git checkout -b feature
或者新版的:
git switch -c feature
這時終端機畫面如下:
.git/ 資料夾如下:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── feature # 4e661dd7f311990102cb56ed907afcffa137f6f8
│ │ └── main # ed1ea06e0f3db055d12ef2c0ea26348ebdab08d1
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/feature
圖示如下:
git checkout 其實包含了以下兩種功能:
git switch。git restore。其中要改變檔案內容的作法包含:
git checkout -- <filename>,同 git restore <filename>。git checkout HEAD -- <filename>(連同預存區一起影響),同 git restore --source=HEAD --staged --worktree <filename>(不影響預存區)。git checkout <commit> -- <filename>,同 git restore --source=<commit> <filename>。而如果直接用 git checkout <commit> 會形成斷頭問題,這時有至少以下兩種解方:
git switch 或 git checkout 把 HEAD 切回去對應分支參考。git switch -c 或 git branch -b,直接在 HEAD 指向的 commit 上新建一個分支參考。因為 git checkout 同時包含兩種功能,在我的習慣中,都會用 git switch 切換分支,避免混淆。